# 一、常见的OOM异常
常见的OOM可以分为以下三类:
- 线程打开过多
- 内存不足(内存溢出或内存泄漏)
- 文件打开过多(IO流处理)
# 1.1 线程OOM
异常日志
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
at java.base/java.lang.Thread.start0(Native Method)
at java.base/java.lang.Thread.start(Thread.java:801)
at java.base/java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:939)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1356)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
at com.company.hpv9.test.main(test.java:11)
2
3
4
5
6
7
排查过程
通过OOM异常日志,可以知道是由于无法创建线程导致的OOM,并且可以根据栈信息定位问题代码的位置。
// 查看当前开启的线程数量
cat proc/{pid}/status
2

优化
- 服务器参数优化
// 查看操作系统对每个进程的线程数限制,可以进行调大
cat /proc/sys/kernel/threads-max
- 业务代码优化
- 替换
new Thread为线程池,控制线程创建数量
- 替换
# 1.2 文件OOM
异常信息
E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
Java.lang.OutOfMemoryError: Could not allocate JNI Env
2
问题分析
系统问题:操作系统对每个进程都有资源的限制,而默认的资源限制通常比较少,需要我们根据业务需求进行调高。
代码问题:如果IO操作有误,没有及时关闭IO流,或者突然大量用户线程同时创建大量文件,也可能出现这个问题。
排查过程
通过异常日志,可以知道是在哪里位置出现了文件创建异常问题,初步检查该位置的代码。
如果没问题,则通过收集到的dumpFd(文件描述符),可以看到很多重复的文件名,进而大致定位到问题。
检查到异常文件名后,再检查该文件的创建位置,进而进行IO流相关代码的检查。
查看文件资源限制
cat /proc/pid/limits

ulimit -n

监控机制
- 文件描述符监控:Linux 系统一切皆文件,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面)。当fd数大于1000个,或者fd连续递增超过50个,就触发fd收集,将fd对应的文件路径上报到后台。
- IO监控:监控完整的IO操作,包括
open、read、write、close,通过在IO操作中插入勾子进行监控。
# 1.3 内存OOM
异常信息
- 堆区
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.company.hpv9.test.main(test.java:13)
2
- 元方法区
// todo
- 堆外内存
nio handle failed java.lang.OutOfMemoryError: Direct buffer memory at org.eclipse.jetty.io.nio.xxxx
at org.eclipse.jetty.io.nio.xxxx at org.eclipse.jetty.io.nio.xxxx
2
问题分析
JVM中JMM发生OOM的区域最常见的是堆区,元方法区和堆外内存。
配置问题:
- 堆区太小(内存溢出):JVM默认的堆区大小比较少,需要通过参数配置调大堆区内存,而堆区又分为年轻代和老年代,可以参考下面的#参数调优进行相关配置。
- Eden区太小 + System.gc关闭(过早晋升):如果年轻代内存分配过小频繁YGC,导致引用堆外内存的对象提前进入老年代,并且老年代没来得及进行GC,则可能导致堆外内存OOM。
代码问题:
- 堆区:代码中的错误处理导致内存泄漏,如ThreadLocal。
- 元方法区:反射的异常使用导致类不断被加载,进而导致元方法区OOM。
- 堆外内存:(部分框架会用到堆外内存,例如Netty)
- 堆外内存泄漏(框架bug-美团Netty案例 (opens new window)):Netty框架源码NPE导致内存泄漏,进而导致堆外内存的OOM,其通过反射监控堆外内存的大小,并逐渐缩小范围,最后找到并处理bug。
排查过程
- 配置JVM参数打印OOM时的堆区信息(
.hprof文件)- 通过MAT工具解析hprof文件
- 通过
Histogram视图可以查看当前的实例占用的内存情况(通常会有占用内存很大的实例对象)- 通过对象引用、GC Root Path等调用链视图,定位问题代码
# 二、MAT内存监控工具
JVM 内存分析工具 MAT 的深度讲解与实践


# 2.1 常见内存分析工具对比
| 产品功能 | MAT | JProfiler | Visual VM | jhat | jmap | hprof |
|---|---|---|---|---|---|---|
| 对象关联分析、深浅堆、GC ROOT、内存泄漏检测、线程分析、提供自定义程序扩展扩展 | Y | N | N | N | N | N |
| 离线全局分析 | Y | N | Y | Y | N | N |
| 内存实时分配情况 | N | Y | Y | Y | Y | Y |
| OQL | Y | N | Y | N | N | N |
| 内存分配堆栈、热点比例 | N | Y | N | N | N | N |
| 堆外内存分析 | N | N | N | N | N | N |
# 2.2 基础概念
Shallow Heap
Shallow Heap 代表一个对象结构自身所占用的内存大小,不包括其属性引用对象所占的内存。如 java.util.ArrayList 对象的 Shallow Heap 包含8字节的对象头、8字节的对象数组属性 elementData 引用 、 4字节的 size 属性、4字节的 modCount 属性。
Retained Set
一个对象的 Retained Set,指的是该对象被 GC 回收后,所有能被回收的对象集合。
Retained Heap
Retained Heap 是一个对象被 GC 回收后,可释放的内存大小,等于释放对象的 Retained Heap 中所有对象的 Shallow Heap 的和。
Dominator tree
Dominator tree 是根据对象引用及支配关系生成的整体树状图,支配树清晰描述了对象间的依赖关系,可以认为是对象的引用树。
- Dominator tree 中任一节点的子树就是被该节点支配的节点集合,也就是其 Retain Set。
OQL
OQL 是类似于 SQL 的 MAT 专用统一查询语言,可以根据复杂的查询条件对 dump 文件中的类或者对象等数据进行查询筛选。
references
outgoing references、incoming references 可以直击对象间依赖关系,MAT 也提供了链式快速操作。
- outgoing references:对象引用的外部对象(注意不包含对象的基本类型属性。基本属性内容可在 inspector 查看)。
- incoming references:直接引用了当前对象的对象,每个对象的 incoming references 可能有 0 到多个。
# 2.3 MAT功能
MAT 的工作原理是对 dump 文件建立多种索引,并基于索引来实现以下四个功能,分别为【内存分布、对象依赖、对象状态和条件检索】,并通过可视化展现辅助 Developer 精细化了解 JVM 堆内存全貌。
[1]内存分布(重点)
- Overview(全局概览信息):堆内存大小、对象个数、类的个数、类加载器的个数、GC root 个数等基本信息。
- Dominator tree(对象引用树):包含对象的内存占用大小及对象的相互引用情况。按对象的
Retain Heap排序,也支持按多个维度聚类统计。
- Histogram(实例内存占比):罗列每个类实例的内存占比,包括自身内存占用量(Shallow Heap)及支配对象的内存占用量(Retain Heap),支持按 package、class loader、super class、class 聚类统计。
- Leak Suspects(可疑泄漏点):MAT提供的问题分析工具,可以简单分析可能出现问题的地方,可解决一些基础问题,但复杂的问题往往帮助有限。
[2]对象间依赖(如实体对象引用关系、线程引用关系、GC Root引用关系等)
- Thread overview(线程调用栈):展现转储 dump 文件时线程栈帧等详细状态,也提供各线程的 Retain Heap 等关联内存信息。(两种方式查看,一种是直接点击上方的Thread overview,查看所有线程信息,一种是在Dominator tree中右键查看其线程信息)
- Dominator tree:该视图即为对象间的引用关系。除了直接用Dominator视图之外(显示了线程或class loader的对象引用关系),还可以在Histogram视图右键如下图,显示该对象及引用了该对象的对象。
- Path To GC Roots(对象到GC Root的引用链路):提供任一对象到GC Root的链路详情,帮助了解不能被 GC 回收的原因
[3]对象状态(内存占用量、字段属性值等)
- inspector面板:可以查看对象的基本信息,包括类名、所在包、父类、类加载器、对象占用内存、GC root信息。下面的Statics可以查看其基本数据类型,对象右键outcoming视图可以查看其调用的外部对象。
- (TODO)集合状态的检测,如:通过 ArrayList 或数组的填充率定位空集合空数组造成的内存浪费、通过 HashMap 冲突率判定 hash 策略是否合理等。
[4]条件检索(OQL、正则匹配查询等)
- 按虚拟内存地址寻址:根据对象的十六进制地址查找对象。
- OQL:提供一种类似于SQL的对象(类)级别统一结构化查询语言。如:查找 size=0 且未使用过的 ArrayList:
select * from java.util.ArrayList where size=0 and modCount=0;查找所有的String的length属性的:select s.length from instanceof String s。

# 三、项目实践
# 3.1 OOM异常的排查流程
- 日志:通过观察日志,再结合最近的上线版本或者更改的代码看能否快速定位到问题。
- Heap Dump:如果无法直接通过日志分析出,则利用内存分析工具(例如MAT)分析Heap Dump文件,通过其内存视图检查是内存溢出还是内存泄漏问题。
- 对象引用关系链:如果是内存泄漏问题,则通过其线程调用链视图、对象引用视图、GC Root链路定位出现内存泄漏的代码区域,并进行修复。
- 参数优化:如果不是内存泄漏问题,即内存中存在的实例都是合理的,那么就需要考虑对内存进行调优,可以从下面几个角度考虑:
- 如果当前物理内存还未分配满,则考虑调大堆区内存大小:
-Xms,-Xmx - 如果物理内存已满,则考虑加大内存或者部署多实例进行负载均衡
- 如果当前物理内存还未分配满,则考虑调大堆区内存大小:
# 3.2 项目遇到的OOM
OOM异常:
- 情报板自动发布大量调用了new Thread,导致无法创建新的线程
解决:
- 通过日志即+最近上线的代码即快速定位到问题代码处
- 该用线程池解决问题
- 解析大XML导致的OOM
TODO
原因:BW源码解析XML存在bug导致了内存泄漏
解决:
- Heap Dump when OOM
- 通过MAT解析堆快照
- 通过实例内存视图定位大内存实例
- 通过对象引用视图定位问题代码
- 引用第三方框架进行XML解析
No Buffer space available
- 大量创建socket连接导致无法创建新的连接(十多万)
原因:与设备通信过程通过TCP进行连接,而每次通信都创建了新的连接,旧的连接又没有释放
解决:
- 日志分析定位问题
- 建立缓存连接管道解决
# 四、参数调优
# 4.1 JVM参数分类
标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
例子:-verbose:class,-verbose:gc,-verbose:jni……
- 非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k……
- 非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps……
# 4.2 常见参数调优
结合个人面试准备中的面试经历模块
内存相关(重点)
对于堆内存大小的调整,JDK官方给了下面的建议进行调整:
- 堆大小(-Xms,-Xmx):老年代存活对象的3-4倍,即【Full GC之后的剩余的老年代内存占用】的3-4倍。
- 元空间大小(-XX:MetaspaceSize,-XX:MaxMetaspaceSize):老年代存活对象的1.2-1.5倍。
- 年轻代大小(-Xmn):老年代存活对象的1-1.5倍。
- 老年代大小:老年代存活对象的2-3倍。

辅助参数:
-XX:+HeapDumpOnOutOfMemoryError:当抛出OOM时进行HeapDump-XX:HeapDumpPath=/home/tmp/logs
大小调整:
-Xms20m:设置jvm初始化堆大小为20m,一般与-Xmx相同,避免动态扩容带来的频繁GC-Xmx20m:设置jvm最大可用内存大小为20m-Xmn20m:设置新生代大小为20m-Xss128k:设置每个线程的栈大小为128k-XX:NewRatio:设置老年代和年轻代的比例,默认是2,即老年代:年轻代为2:1-XX:SurvivorRatio=8:配置年轻代中的eden/survivor的比例为8,即如果新生代内存内存为10M,会分配给Eden-8M,Survivor(from)-1M,Survivor(to)-1M

GC相关
垃圾回收器
-XX:UseSerialGC-XX:+UseConcMarkSweepGC:使用CMS收集器(jdk14已经被移除)-XX:+UseParallelOldGC:使用ParallelGC垃圾回收器-XX:+UseG1GC:使用G1垃圾回收器

-XX:CMSInitiatingOccupancyFraction=80 CMS gc,表示在老年代达到80%使用率时马上进行回收-XX:+PrintGCDetails:打印GC详情- eden space 8192K, 33% used:eden区域总共8192k,使用了33%。2731/8192约等于0.33
- from space 1024K, 0% used
- to space 1024K, 0%used


-XX:+PrintGCTimeStamps:打印时间戳










